/* SSSD Authors: Stephen Gallagher Copyright (C) 2012 Red Hat This program is free software; you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation; either version 3 of the License, or (at your option) any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with this program. If not, see . */ #include #include "src/util/util.h" #include "src/providers/data_provider.h" #include "src/providers/dp_backend.h" #include "src/providers/ad/ad_access.h" #include "src/providers/ad/ad_common.h" #include "src/providers/ldap/sdap_access.h" static void ad_access_done(struct tevent_req *req); static errno_t ad_access_step(struct tevent_req *req, struct sdap_id_conn_ctx *conn); /* * More advanced format can be used to restrict the filter to a specific * domain or a specific forest. This format is KEYWORD:NAME:FILTER * * KEYWORD can be one of DOM or FOREST * KEYWORD can be missing * NAME is a label. * - if KEYWORD equals DOM or missing completely, the filter is applied * for users from domain named NAME only * - if KEYWORD equals FOREST, the filter is applied on users from * forest named NAME only * examples of valid filters are: * apply filter on domain called dom1 only: * dom1:(memberOf=cn=admins,ou=groups,dc=dom1,dc=com) * apply filter on domain called dom2 only: * DOM:dom2:(memberOf=cn=admins,ou=groups,dc=dom2,dc=com) * apply filter on forest called EXAMPLE.COM only: * FOREST:EXAMPLE.COM:(memberOf=cn=admins,ou=groups,dc=example,dc=com) * * If any of the extended formats are used, the filter MUST be enclosed * already. */ /* From least specific */ #define AD_FILTER_GENERIC 0x01 #define AD_FILTER_FOREST 0x02 #define AD_FILTER_DOMAIN 0x04 #define KW_FOREST "FOREST" #define KW_DOMAIN "DOM" /* parse filter in the format domain_name:filter */ static errno_t parse_sub_filter(TALLOC_CTX *mem_ctx, const char *full_filter, char **filter, char **sub_name, int *flags, const int flagconst) { char *specdelim; specdelim = strchr(full_filter, ':'); if (specdelim == NULL) return EINVAL; /* Make sure the filter is already enclosed in brackets */ if (*(specdelim+1) != '(') return EINVAL; *sub_name = talloc_strndup(mem_ctx, full_filter, specdelim - full_filter); *filter = talloc_strdup(mem_ctx, specdelim+1); if (*sub_name == NULL || *filter == NULL) return ENOMEM; *flags = flagconst; return EOK; } static inline errno_t parse_dom_filter(TALLOC_CTX *mem_ctx, const char *dom_filter, char **filter, char **domname, int *flags) { return parse_sub_filter(mem_ctx, dom_filter, filter, domname, flags, AD_FILTER_DOMAIN); } static inline errno_t parse_forest_filter(TALLOC_CTX *mem_ctx, const char *forest_filter, char **filter, char **forest_name, int *flags) { return parse_sub_filter(mem_ctx, forest_filter, filter, forest_name, flags, AD_FILTER_FOREST); } static errno_t parse_filter(TALLOC_CTX *mem_ctx, const char *full_filter, char **filter, char **spec, int *flags) { char *kwdelim, *specdelim; if (filter == NULL || spec == NULL || flags == NULL) return EINVAL; kwdelim = strchr(full_filter, ':'); if (kwdelim != NULL) { specdelim = strchr(kwdelim+1, ':'); if (specdelim == NULL) { /* There is a single keyword. Treat it as a domain name */ return parse_dom_filter(mem_ctx, full_filter, filter, spec, flags); } else if (strncmp(full_filter, "DOM", kwdelim-full_filter) == 0) { /* The format must be DOM:domain_name:filter */ if (specdelim && specdelim-kwdelim <= 1) { /* Check if there is some domain_name */ return EINVAL; } return parse_dom_filter(mem_ctx, kwdelim + 1, filter, spec, flags); } else if (strncmp(full_filter, "FOREST", kwdelim-full_filter) == 0) { /* The format must be FOREST:forest_name:filter */ if (specdelim && specdelim-kwdelim <= 1) { /* Check if there is some domain_name */ return EINVAL; } return parse_forest_filter(mem_ctx, kwdelim + 1, filter, spec, flags); } /* Malformed option */ DEBUG(SSSDBG_CRIT_FAILURE, ("Keyword in filter [%s] did not match expected format\n", full_filter)); return EINVAL; } /* No keyword. Easy. */ *filter = talloc_strdup(mem_ctx, full_filter); if (*filter == NULL) return ENOMEM; *spec = NULL; *flags = AD_FILTER_GENERIC; return EOK; } static errno_t ad_parse_access_filter(TALLOC_CTX *mem_ctx, struct sss_domain_info *dom, const char *filter_list, char **_filter) { char **filters; int nfilters; errno_t ret; char *best_match; int best_flags; char *filter; char *spec; int flags; TALLOC_CTX *tmp_ctx; if (_filter == NULL) return EINVAL; tmp_ctx = talloc_new(mem_ctx); if (tmp_ctx == NULL) { ret = ENOMEM; goto done; } if (filter_list == NULL) { *_filter = NULL; ret = EOK; goto done; } ret = split_on_separator(tmp_ctx, filter_list, '?', true, true, &filters, &nfilters); if (ret != EOK) { DEBUG(SSSDBG_CRIT_FAILURE, ("Cannot parse the list of ad_access_filters\n")); goto done; } best_match = NULL; best_flags = 0; for (int i=0; i < nfilters; i++) { ret = parse_filter(tmp_ctx, filters[i], &filter, &spec, &flags); if (ret != EOK) { /* Skip the faulty filter. At worst, the user won't be * allowed access */ DEBUG(SSSDBG_MINOR_FAILURE, ("Access filter [%s] could not be " "parsed, skipping\n", filters[i])); continue; } if (flags & AD_FILTER_DOMAIN && strcasecmp(spec, dom->name) != 0) { /* If the filter specifies a domain, it must match the * domain the user comes from */ continue; } if (flags & AD_FILTER_FOREST && strcasecmp(spec, dom->forest) != 0) { /* If the filter specifies a forest, it must match the * forest the user comes from */ continue; } if (flags > best_flags) { best_flags = flags; best_match = filter; } } ret = EOK; /* Make sure the result is enclosed in brackets */ *_filter = sdap_get_access_filter(mem_ctx, best_match); done: talloc_free(tmp_ctx); return ret; } struct ad_access_state { struct tevent_context *ev; struct ad_access_ctx *ctx; struct pam_data *pd; struct be_ctx *be_ctx; struct sss_domain_info *domain; char *filter; struct sdap_id_conn_ctx **clist; int cindex; }; static struct tevent_req * ad_access_send(TALLOC_CTX *mem_ctx, struct tevent_context *ev, struct be_ctx *be_ctx, struct sss_domain_info *domain, struct ad_access_ctx *ctx, struct pam_data *pd) { struct tevent_req *req; struct ad_access_state *state; errno_t ret; req = tevent_req_create(mem_ctx, &state, struct ad_access_state); if (req == NULL) { return NULL; } state->ev = ev; state->ctx = ctx; state->pd = pd; state->be_ctx = be_ctx; state->domain = domain; ret = ad_parse_access_filter(state, domain, ctx->sdap_access_ctx->filter, &state->filter); if (ret != EOK) { DEBUG(SSSDBG_CRIT_FAILURE, ("Could not determine the best filter\n")); ret = ERR_ACCESS_DENIED; goto done; } state->clist = ad_gc_conn_list(state, ctx->ad_id_ctx, domain); if (state->clist == NULL) { ret = ENOMEM; goto done; } ret = ad_access_step(req, state->clist[state->cindex]); if (ret != EOK) { goto done; } ret = EOK; done: if (ret != EOK) { tevent_req_error(req, ret); tevent_req_post(req, ev); } return req; } static errno_t ad_access_step(struct tevent_req *req, struct sdap_id_conn_ctx *conn) { struct tevent_req *subreq; struct ad_access_state *state; struct sdap_access_ctx *req_ctx; state = tevent_req_data(req, struct ad_access_state); req_ctx = talloc(state, struct sdap_access_ctx); if (req_ctx == NULL) { return ENOMEM; } req_ctx->id_ctx = state->ctx->sdap_access_ctx->id_ctx; req_ctx->filter = state->filter; memcpy(&req_ctx->access_rule, state->ctx->sdap_access_ctx->access_rule, sizeof(int) * LDAP_ACCESS_LAST); subreq = sdap_access_send(req, state->ev, state->be_ctx, state->domain, req_ctx, conn, state->pd); if (req == NULL) { talloc_free(req_ctx); return ENOMEM; } tevent_req_set_callback(subreq, ad_access_done, req); return EOK; } static void ad_access_done(struct tevent_req *subreq) { struct tevent_req *req; struct ad_access_state *state; errno_t ret; req = tevent_req_callback_data(subreq, struct tevent_req); state = tevent_req_data(req, struct ad_access_state); ret = sdap_access_recv(subreq); talloc_zfree(subreq); switch (ret) { case EOK: tevent_req_done(req); return; case ERR_ACCOUNT_EXPIRED: tevent_req_error(req, ret); return; case ERR_ACCESS_DENIED: /* Retry on ACCESS_DENIED, too, to make sure that we don't * miss out any attributes not present in GC * FIXME - this is slow. We should retry only if GC failed * and LDAP succeeded after the first ACCESS_DENIED */ break; default: break; } /* If possible, retry with LDAP */ state->cindex++; if (state->clist[state->cindex] == NULL) { DEBUG(SSSDBG_OP_FAILURE, ("Error retrieving access check result: %s\n", sss_strerror(ret))); tevent_req_error(req, ret); return; } ret = ad_access_step(req, state->clist[state->cindex]); if (ret != EOK) { tevent_req_error(req, ret); return; } /* Another check in progress */ } static errno_t ad_access_recv(struct tevent_req *req) { TEVENT_REQ_RETURN_ON_ERROR(req); return EOK; } static void ad_access_check_done(struct tevent_req *req); void ad_access_handler(struct be_req *breq) { struct tevent_req *req; struct be_ctx *be_ctx = be_req_get_be_ctx(breq); struct ad_access_ctx *access_ctx = talloc_get_type(be_ctx->bet_info[BET_ACCESS].pvt_bet_data, struct ad_access_ctx); struct pam_data *pd = talloc_get_type(be_req_get_data(breq), struct pam_data); struct sss_domain_info *domain; /* Handle subdomains */ if (strcasecmp(pd->domain, be_ctx->domain->name) != 0) { domain = find_subdomain_by_name(be_ctx->domain, pd->domain, true); if (domain == NULL) { DEBUG(SSSDBG_OP_FAILURE, ("find_subdomain_by_name failed.\n")); be_req_terminate(breq, DP_ERR_FATAL, PAM_SYSTEM_ERR, NULL); return; } } else { domain = be_ctx->domain; } /* Verify that the account is not locked */ req = ad_access_send(breq, be_ctx->ev, be_ctx, domain, access_ctx, pd); if (!req) { be_req_terminate(breq, DP_ERR_FATAL, PAM_SYSTEM_ERR, NULL); return; } tevent_req_set_callback(req, ad_access_check_done, breq); } static void ad_access_check_done(struct tevent_req *req) { errno_t ret; struct be_req *breq = tevent_req_callback_data(req, struct be_req); struct pam_data *pd = talloc_get_type(be_req_get_data(breq), struct pam_data); ret = ad_access_recv(req); talloc_zfree(req); switch (ret) { case EOK: pd->pam_status = PAM_SUCCESS; be_req_terminate(breq, DP_ERR_OK, PAM_SUCCESS, NULL); return; case ERR_ACCESS_DENIED: /* We got the proper denial */ pd->pam_status = PAM_PERM_DENIED; be_req_terminate(breq, DP_ERR_OK, PAM_PERM_DENIED, NULL); return; case ERR_ACCOUNT_EXPIRED: pd->pam_status = PAM_ACCT_EXPIRED; be_req_terminate(breq, DP_ERR_OK, PAM_ACCT_EXPIRED, NULL); return; default: /* Something went wrong */ pd->pam_status = PAM_SYSTEM_ERR; be_req_terminate(breq, DP_ERR_FATAL, PAM_SYSTEM_ERR, sss_strerror(ret)); return; } }