xref: /illumos-gate/usr/src/lib/libsecdb/common/i.rbac (revision 9b9d39d2a32ff806d2431dbcc50968ef1e6d46b2)
1#!/bin/sh
2#
3# CDDL HEADER START
4#
5# The contents of this file are subject to the terms of the
6# Common Development and Distribution License (the "License").
7# You may not use this file except in compliance with the License.
8#
9# You can obtain a copy of the license at usr/src/OPENSOLARIS.LICENSE
10# or http://www.opensolaris.org/os/licensing.
11# See the License for the specific language governing permissions
12# and limitations under the License.
13#
14# When distributing Covered Code, include this CDDL HEADER in each
15# file and include the License file at usr/src/OPENSOLARIS.LICENSE.
16# If applicable, add the following below this CDDL HEADER, with the
17# fields enclosed by brackets "[]" replaced with your own identifying
18# information: Portions Copyright [yyyy] [name of copyright owner]
19#
20# CDDL HEADER END
21#
22# i.rbac
23#
24# Copyright (c) 2005, 2010, Oracle and/or its affiliates. All rights reserved.
25#
26# Script to build RBAC *_attr files from fragments installed by pkg.
27# This script is run by service svc:/system/rbac:default.
28#
29# Related RBAC *_attr files are:
30#
31# /etc/security/{prof_attr,exec_attr,auth_attr}
32# /etc/user_attr
33#
34#  Allowable exit codes
35#
36# 0 - success
37# 2 - warning or possible error condition. Installation continues. A warning
38#     message is displayed at the time of completion.
39#
40
41# i.rbac appears to depend on C locale
42export LC_ALL=C.UTF-8
43
44umask 022
45
46tmp_dir=${TMPDIR:-/tmp}
47
48PATH="/usr/bin:/usr/sbin:${PATH}"
49export PATH
50
51basename_cmd=basename
52cp_cmd=cp
53egrep_cmd=egrep
54mv_cmd=mv
55nawk_cmd=nawk
56rm_cmd=rm
57sed_cmd=sed
58sort_cmd=sort
59
60# $1 is the type
61# $2 is the "old/existing file"
62# $3 is the "new (to be merged)" file
63# $4 is the output file
64# returns 0 on success
65# returns 2 on failure if nawk fails with non-zero exit status
66#
67dbmerge() {
68#
69# Remove the ident lines.
70#
71	${egrep_cmd} -v '^#[pragma 	]*ident' $2 > $4.old 2>/dev/null
72#
73# If the new file has a Sun copyright, remove the Sun copyright from the old
74# file.
75#
76	newcr=`${egrep_cmd} '^# Copyright.*Sun Microsystems, Inc.' $3 \
77	    2>/dev/null`
78	if [ -n "${newcr}" ]; then
79		$sed_cmd -e '/^# Copyright.*Sun Microsystems, Inc./d' \
80		    -e '/^# All rights reserved./d' \
81		    -e '/^# Use is subject to license terms./d' \
82		    $4.old > $4.$$ 2>/dev/null
83		$mv_cmd $4.$$ $4.old
84	fi
85#
86# If the new file has an Oracle copyright, remove both the Sun and Oracle
87# copyrights from the old file.
88#
89	oracle_cr=`${egrep_cmd} '^# Copyright.*Oracle and/or its affiliates.' \
90	    $3 2>/dev/null`
91	if [ -n "${oracle_cr}" ]; then
92		$sed_cmd -e '/^# Copyright.*Sun Microsystems, Inc./d' \
93		    -e '/^# All rights reserved./d' \
94		    -e '/^# Use is subject to license terms./d' \
95		    -e '/^# Copyright.*Oracle and\/or its affiliates./d' \
96		    $4.old > $4.$$ 2>/dev/null
97		$mv_cmd $4.$$ $4.old
98	fi
99#
100# If the new file has the CDDL, remove it from the old file.
101#
102	newcr=`${egrep_cmd} '^# CDDL HEADER START' $3 2>/dev/null`
103	if [ -n "${newcr}" ]; then
104		$sed_cmd -e '/^# CDDL HEADER START/,/^# CDDL HEADER END/d' \
105		    $4.old > $4.$$ 2>/dev/null
106		$mv_cmd $4.$$ $4.old
107	fi
108#
109# Remove empty lines and multiple instances of these comments:
110#
111	$sed_cmd -e '/^# \/etc\/security\/exec_attr/d' -e '/^#$/d' \
112		-e '/^# execution attributes for profiles./d' \
113		-e '/^# See exec_attr([45])/d' \
114		-e '/^# \/etc\/user_attr/d' \
115		-e '/^# user attributes. see user_attr([45])/d' \
116		-e '/^# \/etc\/security\/prof_attr/d' \
117		-e '/^# profiles attributes. see prof_attr([45])/d' \
118		-e '/^# See prof_attr([45])/d' \
119		-e '/^# \/etc\/security\/auth_attr/d' \
120		-e '/^# authorizations. see auth_attr([45])/d' \
121		-e '/^# authorization attributes. see auth_attr([45])/d' \
122		    $4.old > $4.$$
123	$mv_cmd $4.$$ $4.old
124#
125# Retain old and new header comments.
126#
127	$sed_cmd -n -e '/^[^#]/,$d' -e '/^##/,$d' -e p $4.old > $4
128	$rm_cmd $4.old
129	$sed_cmd -n -e '/^[^#]/,$d' -e '/^##/,$d' -e p $3 >> $4
130#
131# If the output file now has both Sun and Oracle copyrights, remove
132# the Sun copyright.
133#
134	sun_cr=`${egrep_cmd} '^# Copyright.*Sun Microsystems, Inc.' \
135	    $4 2>/dev/null`
136	oracle_cr=`${egrep_cmd} '^# Copyright.*Oracle and/or its affiliates.' \
137	    $4 2>/dev/null`
138	if [ -n "${sun_cr}" ] && [ -n "${oracle_cr}" ]; then
139		$sed_cmd -e '/^# Copyright.*Sun Microsystems, Inc./d' \
140		    -e '/^# All rights reserved./d' \
141		    -e '/^# Use is subject to license terms./d' \
142		    $4 > $4.$$ 2>/dev/null
143		$mv_cmd $4.$$ $4
144	fi
145#
146# Handle line continuations (trailing \)
147#
148 	$sed_cmd \
149 	    -e '/\\$/{N;s/\\\n//;}'  -e '/\\$/{N;s/\\\n//;}' \
150 	    -e '/\\$/{N;s/\\\n//;}'  -e '/\\$/{N;s/\\\n//;}' \
151 	    -e '/\\$/{N;s/\\\n//;}'  -e '/\\$/{N;s/\\\n//;}' \
152 	    $2 > $4.old
153 	$sed_cmd \
154 	    -e '/\\$/{N;s/\\\n//;}'  -e '/\\$/{N;s/\\\n//;}' \
155 	    -e '/\\$/{N;s/\\\n//;}'  -e '/\\$/{N;s/\\\n//;}' \
156 	    -e '/\\$/{N;s/\\\n//;}'  -e '/\\$/{N;s/\\\n//;}' \
157 	    $3 > $4.new
158#
159# The nawk script below processes the old and new files using up to
160# three passes.  If the old file is empty, only the final pass over
161# the new file is required.
162#
163	if [ -s $4.old ]; then
164		nawk_pass1=$4.old
165		nawk_pass2=$4.new
166		nawk_pass3=$4.new
167	else
168		nawk_pass1=
169		nawk_pass2=
170		nawk_pass3=$4.new
171	fi
172#
173#!/usr/bin/nawk -f
174#
175#       dbmerge type=[auth|prof|user|exec] [ old-file new-file ] new-file
176#
177#       Merge two versions of an RBAC database file. The output
178#       consists of the lines from the new-file, while preserving
179#       user customizations in the old-file.
180#
181#	Entries in the new-file replace corresponding entries in the
182#	old-file, except as follows:  For exec_attr, all old entries
183#	for profiles contained in the new-file are discarded.  For
184#	user_attr, the "root" entry from the old-file is retained,
185#	and new keywords from the new-file are merged into it.
186#
187#	Records with the same key field(s) are merged, so that the
188#	keyword/value section of each output record contains the union
189#	of the keywords found in all input records with the same key
190#	field(s).  For selected multi-value keywords [1] the values from
191#	the new-file are merged with retained values from the old-file.
192#	Otherwise, the value for each keyword is the final value found
193#	in the new-file, except for keywords in the user_attr entry for
194#	"root" where values from the old-file are always retained.
195#
196#	[1] The following file type and keyword combinations are merged:
197#	    prof_attr: auths, profiles, privs
198#	    user_attr: auths, profiles, roles
199#
200#	The output is run through sort except for the comments
201#	which will appear first in the output.
202#
203#
204	$nawk_cmd  '
205
206# This script may be invoked with up to three file names.  Each file
207# name corresponds to a separate processing pass.  The passes are
208# defined as follows:
209#
210# Pass 1: Read existing data.
211# Data from the old-file is read into memory.
212#
213# Pass 2: Remove obsolete data.
214# Discard any data from the old-file that is part of profiles that
215# are also in the new-file.  (As a special case, the user_attr entry
216# for 'root' is always retained.)
217#
218# Pass 3: Merge new data.
219# Data from the new-file is merged with the remaining old-file data.
220# (As a special case, exec_attr entries are replaced, not merged.)
221
222BEGIN {
223	# The variable 'pass' specifies which type of processing to perform.
224	# When processing only one file, skip passes 1 and 2.
225	if (ARGC == 3)
226		pass += 2;
227
228	# The array 'keyword_behavior' specifies the special treatment of
229	# [type, keyword] combinations subject to value merging.
230	keyword_behavior["prof", "auths"] =	"merge";
231	keyword_behavior["prof", "profiles"] =	"merge";
232	keyword_behavior["prof", "privs"] =	"merge";
233	keyword_behavior["user", "auths"] =	"merge";
234	keyword_behavior["user", "profiles"] =	"merge";
235	keyword_behavior["user", "roles"] =	"merge";
236
237	FS=":"
238}
239
240# When FNR (current file record number) is 1 it indicates that nawk
241# is starting to read the next file specified on its command line,
242# and is beginning the next processing pass.
243FNR == 1 {
244	pass++;
245}
246
247/^#/ || /^$/ {
248	next;
249}
250
251{
252	# For each input line, nawk automatically assigns the complete
253	# line to $0 and also splits the line at field separators and
254	# assigns each field to a variable $1..$n.  Assignment to $0
255	# re-splits the line into the field variables.  Conversely,
256	# assgnment to a variable $1..$n will cause $0 to be recomputed
257	# from the field variable values.
258	#
259	# This code adds awareness of escaped field separators by using
260	# a custom function to split the line into a temporary array.
261	# It assigns the empty string to $0 to clear any excess field
262	# variables, and assigns the desired elements of the temporary
263	# array back to the field variables $1..$7.
264	#
265	# Subsequent code must not assign directly to $0 or the fields
266	# will be re-split without regard to escaped field separators.
267	split_escape($0, f, ":");
268	$0 = "";
269	$1 = f[1];
270	$2 = f[2];
271	$3 = f[3];
272	$4 = f[4];
273	$5 = f[5];
274	$6 = f[6];
275	$7 = f[7];
276}
277
278type == "auth" {
279	key = $1 ":" $2 ":" $3 ;
280	if (pass == 1) {
281		short_comment[key] = $4 ;
282		long_comment[key] = $5;
283		record[key] = $6;
284	} else if (pass == 2) {
285		delete short_comment[key];
286		delete long_comment[key];
287		delete record[key];
288	} else if (pass == 3) {
289		if ( $4 != "" ) {
290			short_comment[key] = $4 ;
291		}
292		if ( $5 != "" ) {
293			long_comment[key] =  $5 ;
294		}
295		record[key] = merge_attrs(record[key], $6);
296	}
297}
298
299type == "prof" {
300	key = $1 ":" $2 ":" $3 ;
301	if (pass == 1) {
302		comment[key] = $4;
303		record[key] = $5;
304	} else if (pass == 2) {
305		delete comment[key];
306		delete record[key];
307	} else if (pass == 3) {
308		if ( $4 != "" ) {
309			comment[key] = $4 ;
310		}
311		if (key != "::") {
312			record[key] = merge_attrs(record[key], $5);
313		}
314	}
315}
316
317type == "exec" {
318	key = $1 ":" $2 ":" $3 ":" $4 ":" $5 ":" $6 ;
319	if (pass == 1) {
320		record[key] = $7;
321	} else if (pass == 2) {
322		# For exec_attr, deletion is based on the 'name' field only,
323		# so that all old entries for the profile are removed.
324		for (oldkey in record) {
325			split_escape(oldkey, oldkey_fields, ":");
326			if (oldkey_fields[1] == $1)
327				delete record[oldkey];
328		}
329	} else if (pass == 3) {
330		# Substitute new entries, do not merge.
331		record[key] = $7;
332	}
333}
334
335type == "user" {
336	key = $1 ":" $2 ":" $3 ":" $4 ;
337	if (pass == 1) {
338		record[key] = $5;
339	} else if (pass == 2) {
340		if ($1 != "root")
341			delete record[key];
342	} else if (pass == 3) {
343		record[key] = merge_attrs(record[key], $5);
344	}
345}
346
347END {
348	for (key in record) {
349		if (type == "prof") {
350			if (key != "::") {
351				print key ":" comment[key] ":" record[key];
352			}
353		} else
354			if (type == "auth") {
355				print key ":" short_comment[key] ":"  \
356				    long_comment[key] ":" record[key];
357			} else
358				print key ":" record[key];
359		}
360}
361
362function merge_attrs(old, new, cnt, new_cnt, i, j, list, new_list, keyword)
363{
364	cnt = split_escape(old, list, ";");
365	new_cnt = split_escape(new, new_list, ";");
366	for (i = 1; i <= new_cnt; i++) {
367		keyword = substr(new_list[i], 1, index(new_list[i], "=")-1);
368		for (j = 1; j <= cnt; j++) {
369			if (match(list[j], "^" keyword "=")) {
370				list[j] = merge_values(keyword, list[j],
371				    new_list[i]);
372				break;
373			}
374		}
375		if (j > cnt)
376			list[++cnt] = new_list[i];
377	}
378
379	return unsplit(list, cnt, ";"); \
380}
381
382function merge_values(keyword, old, new, cnt, new_cnt, i, j, list, new_list, d)
383{
384	# Keywords with multivalued attributes that are subject to merging
385	# are processed by the algorithm implemented further below.
386	# Otherwise, the keyword is not subject to merging, and:
387	#   For user_attr, the existing value is retained.
388	#   For any other file, the new value is substituted.
389	if (keyword_behavior[type, keyword] != "merge") {
390		if (type == "user") {
391			return old;
392		} else {
393			return new;
394		}
395	}
396
397	cnt = split(substr(old, length(keyword)+2), list, ",");
398	new_cnt = split(substr(new, length(keyword)+2), new_list, ",");
399
400	# If the existing list contains "All", remove it and add it
401	# to the new list; that way "All" will appear at the only valid
402	# location, the end of the list.
403	if (keyword == "profiles") {
404		d = 0;
405		for (i = 1; i <= cnt; i++) {
406			if (list[i] != "All")
407				list[++d] = list[i];
408		}
409		if (cnt != d) {
410			new_list[++new_cnt] = "All";
411			cnt = d;
412		}
413	}
414	for (i = 1; i <= new_cnt; i++) {
415		for (j = 1; j <= cnt; j++) {
416			if (list[j] == new_list[i])
417				break;
418		}
419		if (j > cnt)
420			list[++cnt] = new_list[i];
421	}
422
423	return keyword "=" unsplit(list, cnt, ",");
424}
425
426# This function is similar to the nawk built-in split() function,
427# except that a "\" character may be used to escape any subsequent
428# character, so that the escaped character will not be treated as a
429# field separator or as part of a field separator regular expression.
430# The "\" characters will remain in the elements of the output array
431# variable upon completion.
432function split_escape(str, list, fs, cnt, saved, sep)
433{
434	# default to global FS
435	if (fs == "")
436		fs = FS;
437	# initialize empty list, cnt, saved
438	split("", list, " ");
439	cnt = 0;
440	saved = "";
441	# track whether last token was a field separator
442	sep = 0;
443	# nonzero str length indicates more string left to scan
444	while (length(str)) {
445		if (match(str, fs) == 1) {
446			# field separator, terminates current field
447			list[++cnt] = saved;
448			saved = "";
449			str = substr(str, RLENGTH + 1);
450			sep = 1;
451		} else if (substr(str, 1, 1) == "\\") {
452			# escaped character
453			saved = saved substr(str, 1, 2);
454			str = substr(str, 3);
455			sep = 0;
456		} else {
457			# regular character
458			saved = saved substr(str, 1, 1);
459			str = substr(str, 2);
460			sep = 0;
461		}
462	}
463	# if required, append final field to list
464	if (sep || length(saved))
465		list[++cnt] = saved;
466
467	return cnt;
468}
469
470function unsplit(list, cnt, delim, str)
471{
472	str = list[1];
473	for (i = 2; i <= cnt; i++)
474		str = str delim list[i];
475	return str;
476}' \
477	type=$1 $nawk_pass1 $nawk_pass2 $nawk_pass3 > $4.unsorted
478	rc=$?
479	$sort_cmd < $4.unsorted >> $4
480	return $rc
481}
482
483# $1 is the merged file
484# $2 is the target file
485#
486commit() {
487	# Make sure that the last mv uses rename(2) by first moving to
488	# the same filesystem.
489	$mv_cmd $1 $2.$$
490	$mv_cmd $2.$$ $2
491	return $?
492}
493
494outfile=""
495type=""
496set_type_and_outfile() {
497	#
498	# Assumes basename $1 returns one of
499	# prof_attr, exec_attr, auth_attr, or user_attr
500	#
501	fname=`$basename_cmd $1`
502	type=`echo $fname | $sed_cmd -e s'/^\([a-z][a-z]*\)_attr$/\1/' `
503	case "$type" in
504		"prof"|"exec"|"user"|"auth") ;;
505		*) return 2 ;;
506	esac
507
508	outfile=$tmp_dir/rbac_${PKGINST}_${fname}_merge.$$
509
510	return 0
511}
512
513cleanup() {
514	$rm_cmd -f $outfile $outfile.old $outfile.new $outfile.unsorted
515
516	return 0
517}
518
519exit_status=0
520
521# main
522
523while read newfile oldfile ; do
524	if [ -n "$PKGINST" ]
525	then
526		# Install the file in the "fragment" directory.
527		mkdir -m 755 -p ${oldfile}.d
528		rm -f ${oldfile}.d/"$PKGINST"
529		cp $newfile ${oldfile}.d/"$PKGINST"
530
531		# Make sure that it is marked read-only.
532		chmod a-w,a+r ${oldfile}.d/"$PKGINST"
533
534		# We also execute the rest of the i.rbac script.
535	fi
536
537	if [ ! -f $oldfile ]; then
538		cp $newfile $oldfile
539	else
540		set_type_and_outfile $newfile ||
541			set_type_and_outfile $oldfile
542		if [ $? -ne 0 ]; then
543			echo "$0 : $newfile not one of" \
544			    " prof_attr, exec_attr, auth_attr, user_attr"
545			exit_status=2
546			continue
547		fi
548
549		dbmerge $type $oldfile $newfile $outfile
550		if [ $? -ne 0 ]; then
551			echo "$0 : failed to merge $newfile with $oldfile"
552			cleanup
553			exit_status=2
554			continue
555		fi
556
557		commit $outfile $oldfile
558		if [ $? -ne 0 ]; then
559			echo "$0 : failed to mv $outfile to $2"
560			cleanup
561			exit_status=2
562			continue
563		fi
564
565		cleanup
566	fi
567done
568
569if [ "$1" = "ENDOFCLASS" ]; then
570	exit 0
571fi
572
573exit $exit_status
574